import { ref, shallowRef } from 'vue'
// @ts-ignore
import { get as lodashGet, set as lodashSet, cloneDeep } from 'lodash-es'
import { randomId } from './utils.ts'

export type OP = 'add' | 'remove' | 'replace' | 'copy' | 'move'

interface JsonPatch {
    id: string
    op: OP
    path: string
    from?: string
    value?: any
    description: string
}

export const createNavigation = <T>(initialValue: any) => {
    const source = ref<T>(initialValue)
    const redoQueue = shallowRef<JsonPatch[]>([])
    const undoQueue = shallowRef<JsonPatch[]>([])
    const currentIndex = ref<number>()

    const incIndex = () => {
        currentIndex.value!++
    }

    const checkIndex = () => {
        if (currentIndex.value === undefined) {
            currentIndex.value = -1
            return
        }
        if (currentIndex.value !== undoQueue.value.length - 1) {
            redoQueue.value = redoQueue.value.slice(0, currentIndex.value + 1)
            undoQueue.value = undoQueue.value.slice(0, currentIndex.value + 1)
        }
    }

    function parsePath(path: string) {
        const paths = path.split('.')
        const key = paths.pop()
        const parent = paths.join('.')
        return { parent, key }
    }

    function addOp(obj: any, path: string, value: any) {
        const { parent, key } = parsePath(path)
        const parentObj = parent.length ? lodashGet(obj, parent) : obj
        if (Array.isArray(parentObj)) {
            parentObj.splice(Number(key), 0, value)
        } else {
            lodashSet(obj, path, value)
        }
    }

    function removeOp(obj: any, path: string) {
        const { parent, key } = parsePath(path)
        const parentObj = parent.length ? lodashGet(obj, parent) : obj
        if (Array.isArray(parentObj)) {
            parentObj.splice(Number(key), 1)
        } else {
            delete lodashGet(obj, parent)[key]
        }
    }

    function applyOp(obj: any, task: JsonPatch) {
        if (task.op === 'add' || task.op === 'replace') {
            addOp(obj, task.path, task.value)
        }
        if (task.op === 'remove') {
            removeOp(obj, task.path)
        }
        if (task.op === 'move') {
            const fromValue = lodashGet(obj, task.from!)
            removeOp(obj, task.from!)
            addOp(obj, task.path, fromValue)
        }
        if (task.op === 'copy') {
            const fromValue = lodashGet(obj, task.from!)
            addOp(obj, task.path, cloneDeep(fromValue))
        }
    }

    const batch = (tasks: JsonPatch[]) => {
        tasks.forEach(task => {
            applyOp(source.value, task)
        })
    }

    const add = (path: string, value: any, description: string) => {
        checkIndex()
        const task: JsonPatch = { id: randomId(), op: 'add', path, value, description }
        redoQueue.value.push(task)
        undoQueue.value.push({ ...task, op: 'remove' })
        incIndex()
        applyOp(source.value, task)
    }

    const remove = (path: string, description: string) => {
        checkIndex()
        const value = lodashGet(source.value, path)
        const task: JsonPatch = { id: randomId(), op: 'remove', path, value, description }
        redoQueue.value.push(task)
        undoQueue.value.push({ ...task, op: 'add' })
        incIndex()
        applyOp(source.value, task)
    }

    const replace = (path: string, value: any, description: string) => {
        checkIndex()
        const task: JsonPatch = { id: randomId(), op: 'replace', path, value, description }
        redoQueue.value.push(task)
        undoQueue.value.push({ ...task, value: lodashGet(source.value, path) })
        incIndex()
        applyOp(source.value, task)
    }

    const move = (from: string, path: string, description: string) => {
        checkIndex()
        const task: JsonPatch = { id: randomId(), op: 'move', from, path, description }
        redoQueue.value.push(task)
        undoQueue.value.push({ ...task, path: from, from: path })
        incIndex()
        applyOp(source.value, task)
    }

    const copy = (from: string, path: string, description: string) => {
        checkIndex()
        const task: JsonPatch = { id: randomId(), op: 'copy', from, path, description }
        redoQueue.value.push(task)
        undoQueue.value.push({ ...task, op: 'remove' })
        incIndex()
        applyOp(source.value, task)
    }

    const jump = (jumpIndex: number) => {
        if (jumpIndex === currentIndex.value) {
            return
        }
        const tasks = jumpIndex > currentIndex.value!
          ? redoQueue.value.slice(currentIndex.value! + 1, jumpIndex + 1)
          : undoQueue.value.slice(jumpIndex + 1, currentIndex.value! + 1).reverse()
        batch(tasks)
        currentIndex.value = jumpIndex
    }

    const undo = () => {
        if (currentIndex.value === undefined) {
            return
        }
        batch([undoQueue.value[currentIndex.value!]])
        currentIndex.value = currentIndex.value! - 1 < 0 ? undefined : currentIndex.value! - 1
    }

    const redo = () => {
        const newIndex = currentIndex.value === undefined ? 0 : currentIndex.value + 1
        if (newIndex >= redoQueue.value.length) {
            return
        }
        batch([redoQueue.value[newIndex]])
        currentIndex.value = newIndex
    }


    return [
        source,
        {
            add,
            remove,
            replace,
            move,
            copy,
            jump,
            undo,
            redo,
        },
        redoQueue,
        currentIndex,
    ] as const
}
